Android 滑动选择控件 & MVP+Retrofit+RxJava资源推荐
本文作者
作者:超神的菠萝
链接:http://www.jianshu.com/p/baf143364e04
本文由作者投稿发布。
效果图
使用
compile 'com.github.superSp:RulerView:v1.2'
源码地址
https://github.com/superSp/RulerView
1. 初始化画笔,以及其他需要的参数
2. 重写onMeasuer()确定尺子的大小
3. 重写onDraw()绘画出静态尺子,并且将一些滑动时需要改变的参数设置为变量,绘制时只绘制当前屏幕可见区域,滑动尺子时,只刷新当前屏幕模拟滑动并不是真正的滑动
4. 重写onTouchEvent()处理滑动,增加滑动速率监听VelocityTracker以及惯性滑动以及抬起手指时指针落在刻度上面需要的属性动画ValueAnimator
测量
控件的高度=尺子的高度+结果值的高度+尺子距离结果值的高度
控件的宽度=屏幕宽度或者固定宽度
void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightModule = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
switch (heightModule) {
case MeasureSpec.AT_MOST:
height = rulerHeight
+ (showScaleResult ? resultNumRect.height() : 0)
+ rulerToResultgap * 2 + getPaddingTop()
+ getPaddingBottom();
break;
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.EXACTLY:
height = heightSize + getPaddingTop() + getPaddingBottom();
break;
}
width = widthSize + getPaddingLeft() + getPaddingRight();
setMeasuredDimension(width, height);
}
测量这个环节很重要的就是定位自己这个控件的宽高的具体使用方式,例如本控件的宽度,因为内部可以滑动,是没有办法设置为wrap_content的(除非有个默认值)。
绘制静态尺子
绘制背景
private void drawBg(Canvas canvas) {
bgRect.set(0, 0, width, height);
if (isBgRoundRect) {
/*20->椭圆的用于圆形角x-radius*/
canvas.drawRoundRect(bgRect, 20, 20, bgPaint);
} else {
canvas.drawRect(bgRect, bgPaint);
}
}
绘制那个白色的圆角/矩形背景。
绘制尺子
这一步是绘制控件中最为复杂的一步,需要考虑初始如何默认选中初始刻度,手指抬起时候尺子需要滑动到的位置,计算当前所处刻度等等。
绘制滑动类型的view时,当初的想法是一次性绘制出全部内容,之后使用canvas.clipRect()裁剪掉不可见区域,但是如果内容区域比较大,例如需要绘制1000个内容,则没滑动一次for循环需要执行1000次,而且刻度越大时候循环越多,占用内存更大,会造成卡顿,因此换了另外一种思路,只绘制当前屏幕可见区域内容,这样无论刻度有多大,暂用的内存都很小,滑动时,通过不断刷新来模拟滑动,做到以假乱真的效果。
private void drawScaleAndNum(Canvas canvas) {
canvas.translate(0, (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap);//移动画布到结果值的下面
//确定刻度位置
int num1;
float num2;
//第一次进来的时候计算出默认刻度对应的假设滑动的距离moveX
if (firstScale != -1) {
//如果设置了默认滑动位置,计算出需要滑动的距离
moveX = getWhichScalMovex(firstScale);
lastMoveX = moveX;
//将结果置为-1,下次不再计算初始位置
firstScale = -1;
}
//滑动刻度的整数部分
num1 = -(int) (moveX / scaleGap);
//滑动刻度的小数部分
num2 = (moveX % scaleGap);
canvas.save();
//准备开始绘制当前屏幕,从最左面开始
rulerRight = 0;
if (isUp) { //这部分代码主要是计算手指抬起时,惯性滑动结束时,刻度需要停留的位置
//计算滑动位置距离整点刻度的小数部分距离
num2 = ((moveX - width / 2 % scaleGap) % scaleGap);
if (num2 <= 0) {
num2 = scaleGap - Math.abs(num2);
}
//当前滑动位置距离左边整点刻度的距离
leftScroll = (int) Math.abs(num2);
//当前滑动位置距离右边整点刻度的距离
rightScroll = (int) (scaleGap - Math.abs(num2));
//最终计算出当前位置到整点刻度位置需要滑动的距离
float moveX2 = num2 <= scaleGap / 2 ? moveX - leftScroll : moveX + rightScroll;
//手指抬起,并且当前没有惯性滑动在进行,创建一个惯性滑动
if (valueAnimator != null && !valueAnimator.isRunning()) {
valueAnimator = ValueAnimator.ofFloat(moveX, moveX2);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
//不断滑动去更新新的位置
moveX = (float) animation.getAnimatedValue();
lastMoveX = moveX;
invalidate();
}
});
//增加一个监听,用来返回给使用者滑动结束后的最终结果刻度值
valueAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//这里是滑动结束时候回调给使用者的结果值
if (onChooseResulterListener != null) {
onChooseResulterListener.onEndResult(resultText);
}
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
isUp = false;
}
//重新计算当前滑动位置的整数以及小数位置
num1 = (int) -(moveX / scaleGap);
num2 = (moveX % scaleGap);
}
//不加该偏移的话,滑动时刻度不会落在0~1之间只会落在整数上面,其实这个都能设置一种模式了,毕竟初衷就是指针不会落在小数上面
canvas.translate(num2, 0);
//这里是滑动时候不断回调给使用者的结果值
resultText = String.valueOf(new WeakReference<>(new BigDecimal((width / 2 - moveX) / (scaleGap * scaleCount))).get().setScale(1, BigDecimal.ROUND_HALF_UP).floatValue() + minScale);
if (onChooseResulterListener != null) {
//接口不断回调给使用者结果值
onChooseResulterListener.onScrollResult(resultText);
}
//绘制当前屏幕可见刻度,不需要裁剪屏幕,while循环只会执行·屏幕宽度/刻度宽度·次,
// 大部分的绘制都是if(curDis<width)这样子内存暂用相对来说会比较高。。
while (rulerRight < width) {
if (num1 % scaleCount == 0) { //绘制整点刻度以及文字
if ((moveX >= 0 && rulerRight < moveX - scaleGap)
|| width / 2 - rulerRight <= getWhichScalMovex(maxScale + 1) - moveX) {
//当滑动出范围的话,不绘制,去除左右边界
} else {
//绘制刻度,绘制刻度数字
canvas.drawLine(0, 0, 0, midScaleHeight, midScalePaint);
scaleNumPaint.getTextBounds(num1 / scaleGap
+ minScale + "", 0, (num1 / scaleGap + minScale + "").length(), scaleNumRect);
canvas.drawText(num1 / scaleCount + minScale
+ "", -scaleNumRect.width() / 2, lagScaleHeight +
(rulerHeight - lagScaleHeight) / 2 + scaleNumRect.height(), scaleNumPaint);
}
} else { //绘制小数刻度
if ((moveX >= 0 && rulerRight < moveX)
|| width / 2 - rulerRight < getWhichScalMovex(maxScale) - moveX) {
//当滑动出范围的话,不绘制,去除左右边界
} else {
//绘制小数刻度
canvas.drawLine(0, 0, 0, smallScaleHeight, smallScalePaint);
}
}
++num1; //刻度加1
rulerRight += scaleGap; //绘制屏幕的距离在原有基础上+1个刻度间距
canvas.translate(scaleGap, 0); //移动画布到下一个刻度
}
canvas.restore();
//绘制屏幕中间用来选中刻度的最大刻度
canvas.drawLine(width / 2, 0, width / 2, lagScaleHeight, lagScalePaint);
}
处理滑动
主要是记录moveX,以及添加velocityTracker速度监听器,以及处理惯性滑动
@Override
public boolean onTouchEvent(MotionEvent event) {
currentX = event.getX();
isUp = false;
velocityTracker.computeCurrentVelocity(500);
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下时如果属性动画还没执行完,就终止,记录下当前按下点的位置
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.end();
valueAnimator.cancel();
}
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
//滑动时候,通过假设的滑动距离,做超出左边界以及右边界的限制。
moveX = currentX - downX + lastMoveX;
if (moveX >= width / 2) {
moveX = width / 2;
} else if (moveX <= getWhichScalMovex(maxScale)) {
moveX = getWhichScalMovex(maxScale);
}
break;
case MotionEvent.ACTION_UP:
//手指抬起时候制造惯性滑动
lastMoveX = moveX;
xVelocity = (int) velocityTracker.getXVelocity();
autoVelocityScroll(xVelocity);
velocityTracker.clear();
break;
}
invalidate();
return true;
}
处理惯性滑动的代码
这里就是调节了,根据得到的速率调节出比较舒服的滑动
private void autoVelocityScroll(int xVelocity) {
//惯性滑动的代码,速率和滑动距离,以及滑动时间需要控制的很好,应该网上已经有关于这方面的算法了吧。
// 这里是经过N次测试调节出来的惯性滑动
if (Math.abs(xVelocity) < 50) {
isUp = true;
return;
}
if (valueAnimator.isRunning()) {
return;
}
valueAnimator = ValueAnimator.ofInt(0, xVelocity / 20).setDuration(Math.abs(xVelocity / 10));
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
moveX += (int) animation.getAnimatedValue();
if (moveX >= width / 2) {
moveX = width / 2;
} else if (moveX <= getWhichScalMovex(maxScale)) {
moveX = getWhichScalMovex(maxScale);
}
lastMoveX = moveX;
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
isUp = true;
invalidate();
}
});
valueAnimator.start();
}
供外部使用的获取结果值的接口
public interface OnChooseResulterListener {
void onEndResult(String result); //结束滑动时候返回的结果
void onScrollResult(String result); //滑动时不断产生的结果
}
源码地址
https://github.com/superSp/RulerView
光看一篇自定义控件,你可能还不满足,毕竟全是代码看起来挺费劲的。刚好后台有很多朋友想学习MVP+Retrofit+RxJava的一些组合,这里给大家找了一些文章,比较适合目前还不太了解的同学:
带你高效学习MVP+RxJava+Retrofit
http://www.jianshu.com/p/c81c48144029
Android 教你一步步搭建MVP+Retrofit+RxJava网络请求框架http://www.jianshu.com/p/7b839b7c5884
MVP+Retrofit+Rxjava在项目中实战解析
http://www.jianshu.com/p/644206ddbd2c
小白能看懂的MVP+RXjava+Retrofit2详细讲解http://www.jianshu.com/p/426864584518
MVP+Retrofit+Rxjava在项目中实战解析https://juejin.im/post/596eb0faf265da6c322e0e3d
MVP + Retrofit + RxJava 优雅的实现http://lovehaodong.cn/2017/04/12/Android%20%E6%9E%B6%E6%9E%84/
Retrofit+Rxjava+Okhttp+MVP搭建Android开发框架一
http://t.cn/RYe617d
Retrofit-Rxjava-Okhttp-MVP搭建Android开发框架二
http://t.cn/RYe6gRA
刚好是周五,建议如果从未有过尝试的,可以周末好好学习了解下。而且经常推荐的开源项目多数都是以此为基础的;因为涉及的技术比较多,光看起来比较费劲,建议跟着敲,边敲便体会。
当然了,微信内不支持外链,你可以在玩Android上访问,专为MVP+RXjava+Retrofit2设置了一个章节,也更加便于你收藏文章:
http://www.wanandroid.com/article/list/0?cid=260
推荐阅读:
上一篇:高级MVP架构封装演变全过程
Google、滴滴 与 Udacity 联合开发的 Android 课程,有来自硅谷的实战项目,并提供一对一代码审阅和技术辅导,现在部分课程能免费体验,感兴趣的朋友可以扫下面的二维码。